﻿namespace Xompare
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Drawing;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Windows.Forms;
    using System.Xml;
    using System.Xml.Linq;

    using FormsUtilities;

    using Utilities;
    using Utilities.FileChangesTracking;

    using Xompare.Helpers;

    /// <summary>
    /// The available roles of the XML file viewer in the comparison.
    /// </summary>
    public enum FileComparisonRole
    {
        /// <summary>
        /// This tree representation represents the original side of a comparison.
        /// </summary>
        OriginSide,

        /// <summary>
        /// This tree representation represents the target side of a comparison.
        /// </summary>
        TargetSide
    }

    public enum NodeRepresentation
    {
        /// <summary>
        /// The node represents no difference in the comparison.
        /// </summary>
        Natural,

        /// <summary>
        /// The node represents information only on this side of the comparison.
        /// </summary>
        OnlyHere,

        /// <summary>
        /// The node represents information similar but different on both sides of the comparison.
        /// </summary>
        Difference,

        /// <summary>
        /// The node is a parent to nodes with various comparison information representation.
        /// </summary>
        GroupVariance
    }

    public enum SearchDirection
    {
        Forward,
        Backward
    }

    public partial class XmlFileTreeControl : UserControl
    {
        #region Tree node colors

        /// <summary>
        /// The fore-color given to a node which has no representation of difference information.
        /// </summary>
        private static readonly Color s_naturalForeColor = Color.Black;

        /// <summary>
        /// The back-color given to a node which has no representation of difference information.
        /// </summary>
        private static readonly Color s_naturalBackColor = Color.White;

        /// <summary>
        /// The back-color given to a node which has some of its decendents marked as a various types of differences.
        /// </summary>
        private static readonly Color s_groupVarianceColor = Color.FromArgb(0xFF, 0xCA, 0xFA, 0xFF);

        /// <summary>
        /// The fore-color of a tree node marked as belonging only to the original side of the comparison.
        /// </summary>
        private static readonly Color s_onlyOriginalColor = Color.Red;

        /// <summary>
        /// The fore-color of a tree node marked as belonging only to the target side of the comparison.
        /// </summary>
        private static readonly Color s_onlyTargetColor = Color.FromArgb(0xFF, 0, 0xD0, 0);

        /// <summary>
        /// The fore-color of a tree node marked as belonging to both sides of the comparison, only they differ.
        /// </summary>
        private static readonly Color s_differentColor = Color.MediumPurple;

        /// <summary>
        /// The back-color of a tree node marking it is the difference counter-part of the node selected by the user on the other side of the comparison.
        /// </summary>
        private static readonly Color s_counterPartDiffColor = Color.Yellow;

        /// <summary>
        /// The back-color of a tree node which is a search result.
        /// </summary>
        private static readonly Color s_searchMark = Color.DarkOrange;

        /// <summary>
        /// The back-color of a tree node which was last selected by the user, before the control lost focus.
        /// </summary>
        private static readonly Color s_selectedLostFocus = Color.AliceBlue;

        /// <summary>
        /// The back-color of a tree node which some ancestor was added.
        /// </summary>
        private static readonly Color s_blockAdded = Color.FromArgb(0xFF, 0xEB, 0xFF, 0xE3);

        /// <summary>
        /// The back-color of a tree node which some ancestor was removed.
        /// </summary>
        private static readonly Color s_blockRemoved = Color.FromArgb(0xFF, 0xFF, 0xE2, 0xE2);

        #endregion

        /// <summary>
        /// The list of tree-nodes currently marked for some reason.
        /// </summary>
        private readonly List<TreeNode> m_markedNodes = new List<TreeNode>();

        /// <summary>
        /// The last node-search result.
        /// </summary>
        private readonly List<TreeNode> m_searchResult = new List<TreeNode>();

        /// <summary>
        /// Tracks the last user selected <see cref="TreeNode"/>.
        /// </summary>
        private TreeNode m_lastSelectedNode = null;

        /// <summary>
        /// The name and label of the XML file.
        /// </summary>
        private NameAndLabel m_filePath = new NameAndLabel(string.Empty, null);

        /// <summary>
        /// Used when searching for tree nodes. Denotes whether the search hit the starting point.
        /// </summary>
        private bool m_searchHitStartPoint = false;

        /// <summary>
        /// The ID of the change handler for file changes.
        /// </summary>
        private Guid m_fileChangeHandlerId;

        #region Exported events

        /// <summary>
        /// Defines the event of changing the <see cref="XmlFileTreeControl.FilePath"/> property.
        /// </summary>
        public event Action OnFilePathChanged;

        /// <summary>
        /// Defines the event of selecting a tree-node representing a difference in the XML comparison.
        /// </summary>
        public event Action<XmlFileTreeControl, NodeInformation, bool> OnDiffNodeSelected;

        #endregion

        public XmlFileTreeControl()
        {
            InitializeComponent();

            var executingAssembly = Assembly.GetExecutingAssembly();

            treeXml.PathSeparator = new string(ParsedXmlTree.PathSeparator, 1);
            treeXml.ImageList = new ImageList();
            treeXml.ImageList.Images.Add(MiscResources.GetImage(executingAssembly, "XElementImage.bmp"));
            treeXml.ImageList.Images.Add(MiscResources.GetImage(executingAssembly, "XAttributeImage.bmp"));

            PreParseSettings = new ParsedXmlTreeInitialSettings { DefaultKeyName = Controller.GetDefaultKey("*") };

            ViewXmlSyntax = true;
        }

        /// <summary>
        /// Gets or sets the name and label related to this XML tree control.
        /// </summary>
        public NameAndLabel FilePath
        {
            get
            {
                return m_filePath;
            }

            set
            {
                if (value == null)
                {
                    return;
                }

                string safePath = ValidateFilePath(value.Name);
                if (!string.IsNullOrEmpty(safePath))
                {
                    var doUpdateFileWatcher = !m_filePath.Name.Equals(value.Name);

                    if (!string.IsNullOrEmpty(m_filePath.Name) && doUpdateFileWatcher)
                    {
                        // Unsubscribe the last file:
                        FileChangeTrackingManager.UnsubscribeChangeHandler(m_filePath.Name, m_fileChangeHandlerId);
                    }

                    m_filePath = new NameAndLabel(safePath, value.Label);

                    if (doUpdateFileWatcher)
                    {
                        m_fileChangeHandlerId = FileChangeTrackingManager.RegisterFileHandler(
                            m_filePath.Name,
                            (changeType, changedPath, newPath) =>
                            {
                                switch (changeType)
                                {
                                    case FileChangeType.FileChanged:
                                    {
                                        if (LogUi.LogQuestion(
                                            MessageBoxButtons.YesNo,
                                            "The following file was changed outside the comparer. Do you want to reload?{0}{1}",
                                            Environment.NewLine,
                                            m_filePath.Name) == DialogResult.Yes)
                                        {
                                            FilePath = FilePath; // Refresh the view
                                        }

                                        break;
                                    }

                                    case FileChangeType.FileDeleted:
                                    {
                                        LogUi.LogInfo("The following file was deleted outside the comparer:{0}{1}", Environment.NewLine, m_filePath.Name);

                                        m_filePath = new NameAndLabel(m_filePath.Name, "DELETED: " + m_filePath.Name);
                                        labeledTextFilePath.Label = m_filePath.Label;
                                        labeledTextFilePath.ShowLabel();

                                        break;
                                    }

                                    case FileChangeType.FileRenamed:
                                    {
                                        if (LogUi.LogQuestion(
                                            MessageBoxButtons.YesNo,
                                            "File '{0}' was renamed to '{1}' outside the comparer. Reloding will remove the comparison. Do you want to reload?",
                                            m_filePath.Name,
                                            newPath) == DialogResult.Yes)
                                        {
                                            FilePath = FilePath; // Refresh the view
                                        }

                                        break;
                                    }
                                }
                            });
                    }

                    OnFilePathChanged();
                }

                labeledTextFilePath.InternalText = m_filePath.Name;
                labeledTextFilePath.Label = m_filePath.Label;
                labeledTextFilePath.Refresh();
            }
        }

        /// <summary>
        /// Gets the XML document relate to the file maintained (according to <see cref="FilePath"/>).
        /// </summary>
        public XDocument FileXmlDocument { get; private set; }

        /// <summary>
        /// Gets the settings object for the <see cref="PreParseInformation"/> object property.
        /// </summary>
        public ParsedXmlTreeInitialSettings PreParseSettings { get; private set; }

        /// <summary>
        /// Gets the pre-parsing information of the XML document for later getting comparison information.
        /// </summary>
        public ParsedXmlTree PreParseInformation { get; private set; }

        /// <summary>
        /// Gets or sets the role in the comparison that this XML tree viewer is handling.
        /// </summary>
        public FileComparisonRole FileViewerRole { get; set; }

        /// <summary>
        /// Gets or sets the map of XML node entries available only in this XML describer.
        /// </summary>
        public IReadOnlyDictionary<string, ComparableSet<KeyedXmlRecord>> XmlOnlyHere { get; set; }

        /// <summary>
        /// Gets or sets the map of XML node entries available only in the other XML describer.
        /// </summary>
        public IReadOnlyDictionary<string, ComparableSet<KeyedXmlRecord>> XmlOnlyThere { get; set; }

        /// <summary>
        /// Gets or sets the set of XML node differences of the comparison.
        /// </summary>
        public IReadOnlyDictionary<string, Tuple<ComparableSet<KeyedXmlRecord>, ComparableSet<KeyedXmlRecord>>> XmlDifferences { get; set; }

        /// <summary>
        /// Gets or sets whether to display the tree-node text as XML syntax.
        /// </summary>
        public bool ViewXmlSyntax { get; set; }

        /// <summary>
        /// Things to do upon removal of the control.
        /// </summary>
        internal void OnRemoval()
        {
            FileChangeTrackingManager.UnsubscribeChangeHandler(m_filePath.Name, m_fileChangeHandlerId);
        }

        #region Internal functionality

        /// <summary>
        /// Find a node in the tree according to the given node-information and highlights it. If nothing is found, nothing is highlighted.
        /// </summary>
        /// <param name="treeNodeInfo">The information by which to find the required tree-node.</param>
        /// <returns>The node-information instance of the selected tree node, or 'null' if none selected.</returns>
        internal NodeInformation EnsureNodeVisible(NodeInformation treeNodeInfo)
        {
            return EnsureNodeVisibleByXmlPath(treeNodeInfo.XmlPath);
        }

        /// <summary>
        /// Finds a node in the tree which tagged XML path matches that of the given one, and highlights it. If nothing is found, nothing is highlighed.
        /// </summary>
        /// <param name="xmlPath">The XML path by which to find the tree-node.</param>
        /// <returns>The node-information instance of the selected tree node, or 'null' if none selected.</returns>
        internal NodeInformation EnsureNodeVisibleByXmlPath(string xmlPath)
        {
            var matchedNodes = new List<TreeNode>();

            FindNodeByXmlPath(xmlPath, matchedNodes);
            if (!matchedNodes.Any())
            {
                FindNodeByXmlPath(KeyedXmlRecord.GetElementPathWithoutFirstKey(xmlPath), matchedNodes);

                // If when searching without the key-value information results in more than one node found - the information is not relevant:
                if (matchedNodes.Count > 1)
                {
                    matchedNodes.Clear();
                }
            }

            // Unmark the last selection:
            ClearNodeCollection(m_markedNodes, node => node.IsCounterChosen = false);

            // Mark the new selection:
            if (matchedNodes.Any())
            {
                foreach (var matchedNode in matchedNodes)
                {
                    var nodeInformation = matchedNode.Tag as NodeInformation;

                    if (nodeInformation != null)
                    {
                        nodeInformation.IsCounterChosen = true;
                        ColorNode(matchedNode);

                        m_markedNodes.Add(matchedNode);
                    }
                }

                matchedNodes.First().EnsureVisible();

                return matchedNodes.First().Tag as NodeInformation;
            }

            Trace.TraceWarning("Could not find tree nodes in {0} tree, XML path {1}", FileViewerRole, xmlPath);

            return null;
        }

        /// <summary>
        /// Sets the XML tree in the user-control UI.
        /// </summary>
        internal void RefreshXmlTree()
        {
            if (FileXmlDocument == null || FileXmlDocument.Root == null)
            {
                // Empty XML document?
                return;
            }

            treeXml.Nodes.Clear();
            var rootNode = new TreeNode(FileXmlDocument.Root.Name.LocalName, imageIndex: 0, selectedImageIndex: 0)
            {
                Tag = new NodeInformation(FileXmlDocument.Root) { XmlPath = ParsedXmlTree.CombinePath(FileXmlDocument.Root.Name.ToString()) }
            };

            MarkNode(rootNode, FileXmlDocument.Root);
            treeXml.Nodes.Add(rootNode);

            PopulateTreeElement(rootNode, FileXmlDocument.Root);
        }

        /// <summary>
        /// Validates, sets the path to the XML file, and returns the absolute path used.
        /// Prepares the XML document.
        /// Does not set the text on the <see cref="labeledTextFilePath"/> text box.
        /// </summary>
        /// <param name="filePath">The path of the XML file to use.</param>
        /// <returns>The actual absolute path used, or empty string if not usable.</returns>
        internal string ValidateFilePath(string filePath)
        {
            if (string.IsNullOrEmpty(filePath))
            {
                LogUi.LogError("There was no file name specified.");

                return string.Empty;
            }

            string absolutePath;

            try
            {
                absolutePath = Path.GetFullPath(filePath);
            }
            catch (Exception ex)
            {
                LogUi.LogError("Given file name '{0}' is not valid, Error: {1}", filePath, ex.Message);

                return string.Empty;
            }

            if (!File.Exists(absolutePath))
            {
                LogUi.LogError("File '{0}' does not exist.", absolutePath);

                return string.Empty;
            }

            if (File.GetAttributes(absolutePath) == FileAttributes.Directory)
            {
                LogUi.LogError("Path '{0}' is a directory.", absolutePath);

                return string.Empty;
            }

            try
            {
                FileXmlDocument = XDocument.Load(absolutePath);
                var fileExtension = Path.GetExtension(absolutePath);

                PreParseSettings.DefaultKeyName = Controller.GetDefaultKey("*" + fileExtension);
                PreParseInformation = new ParsedXmlTree(FileXmlDocument, PreParseSettings);
            }
            catch (XmlException xmlEx)
            {
                LogUi.LogError("XML file '{0}' is invalid: {1}", absolutePath, xmlEx.Message);

                return string.Empty;
            }
            catch (Exception ex)
            {
                LogUi.LogFatal(ex.Message);

                return string.Empty;
            }

            return absolutePath;
        }

        #endregion

        #region Private helpers

        /// <summary>
        /// Find child tree-nodes corresponding to the given XML path as denoted in the node's tag.
        /// </summary>
        /// <param name="xmlPath">The identifying XML path.</param>
        /// <param name="matchedNodes">Aggregates the list of matching tree-nodes.</param>
        private void FindNodeByXmlPath(string xmlPath, List<TreeNode> matchedNodes)
        {
            // Assuming there's only one root to the tree:
            var rootNode = treeXml.Nodes[0];
            var xmlPathSeparated = ParsedXmlTree.SplitPath(xmlPath, 2);
            var rootXmlPath = MiscUtils.AdjustCaption(((NodeInformation)rootNode.Tag).XmlPath, PreParseInformation.IgnoreNameCase, PreParseInformation.IgnoreWhiteSpaces);

            if (xmlPathSeparated.First().Equals(ParsedXmlTree.SplitPath(rootXmlPath).First()) && xmlPathSeparated.Count() > 1)
            {
                FindNodeByXmlPath(rootNode, xmlPathSeparated[1], 1, matchedNodes);
            }
        }

        /// <summary>
        /// Find child tree-nodes corresponding to the given XML path as denoted in the node's tag.
        /// </summary>
        /// <param name="root">The root of the path.</param>
        /// <param name="xmlPath">The XML path of the required node.</param>
        /// <param name="depth">The depth in the original path.</param>
        /// <param name="result">The list of nodes matching the required XML path.</param>
        private void FindNodeByXmlPath(TreeNode root, string xmlPath, int depth, List<TreeNode> result)
        {
            var xmlPathSeparated = ParsedXmlTree.SplitPath(xmlPath, 2);

            if (!xmlPathSeparated.Any())
            {
                return;
            }

            var nodeCollection = root.Nodes.Cast<TreeNode>().ToList();

            if (!nodeCollection.Any())
            {
                if (!result.Any() || result.Any(node => root.Level > node.Level))
                {
                    result.Clear();
                    result.Add(root);
                }

                return;
            }

            var potentialNodes = nodeCollection.Where(
                node =>
                {
                    var nodeXmlSeparated = ParsedXmlTree.SplitPath(((NodeInformation)node.Tag).XmlPath);

                    if (nodeXmlSeparated.Count() <= depth)
                    {
                        return false;
                    }

                    var nodeNameTag = MiscUtils.AdjustCaption(nodeXmlSeparated[depth], PreParseInformation.IgnoreNameCase, PreParseInformation.IgnoreWhiteSpaces);
                    if (nodeNameTag.Equals(xmlPathSeparated.First()))
                    {
                        return true;
                    }

                    nodeNameTag = KeyedXmlRecord.GetElementPathWithoutFirstKey(nodeNameTag);
                    return nodeNameTag.Equals(xmlPathSeparated.First());
                }).ToList();

            if (xmlPathSeparated.Count() == 1)
            {
                if (potentialNodes.Any() && (!result.Any() || result.Any(node => potentialNodes.First().Level > node.Level)))
                {
                    result.Clear();
                    result.AddRange(potentialNodes);
                }

                return;
            }

            // There're more parts to the XML path, keep digging:
            foreach (var potentialNode in potentialNodes)
            {
                FindNodeByXmlPath(potentialNode, xmlPathSeparated[1], depth + 1, result);
            }
        }

        /// <summary>
        /// Clears a collection of tree-nodes, while also reseting their node-coloring.
        /// </summary>
        /// <param name="nodeCollection">The node-collection to clear.</param>
        /// <param name="nodeClearAction">The action to perform on the node-information tag for each cleared tree-node.</param>
        private void ClearNodeCollection(List<TreeNode> nodeCollection, Action<NodeInformation> nodeClearAction)
        {
            foreach (var node in nodeCollection)
            {
                var markedInformation = node.Tag as NodeInformation;

                if (markedInformation != null)
                {
                    nodeClearAction(markedInformation);
                    ColorNode(node);
                }
            }

            nodeCollection.Clear();
        }

        /// <summary>
        /// Allow the user to choose an input XML file.
        /// </summary>
        private void UserChooseFileInput()
        {
            var fileDialog = new OpenFileDialog();

            switch (FileViewerRole)
            {
                case FileComparisonRole.OriginSide:
                {
                    fileDialog.Title = "Choose the original XML file";

                    break;
                }

                case FileComparisonRole.TargetSide:
                {
                    fileDialog.Title = "Choose the target XML file";

                    break;
                }

                default:
                {
                    LogUi.LogFatal("There's no such role '{0}'", FileViewerRole);

                    break;
                }
            }

            fileDialog.ShowDialog();
            if (!string.IsNullOrEmpty(fileDialog.FileName))
            {
                FilePath = new NameAndLabel(fileDialog.FileName, null);

                RefreshXmlTree();
            }
        }

        /// <summary>
        /// Populate a tree node with relevant XML XElement node.
        /// Each node's tag contains the path as expected by the comparer calculation results.
        /// </summary>
        /// <param name="node">The UI tree node.</param>
        /// <param name="xNode">The data XML XElement node.</param>
        private void PopulateTreeElement(TreeNode node, XElement xNode)
        {
            var nodeInformation = (NodeInformation)node.Tag;
            var parentXmlPath = nodeInformation.XmlPath;

            // Create and mark child XML attributes:
            if (xNode.HasAttributes)
            {
                bool needAdditionComparison = false;
                CollectionComparer<XName, string> additionComparer = null;

                // Check for addition based comparison for the attribute-childs:
                if (XmlOnlyHere != null && XmlOnlyThere != null)
                {
                    needAdditionComparison = PreParseInformation.CreateAdditionComparer(
                        parentXmlPath,
                        XmlOnlyHere,
                        XmlOnlyThere,
                        out additionComparer);

                    if (needAdditionComparison && FileViewerRole == FileComparisonRole.TargetSide)
                    {
                        // Switch the sides of the addition comparer:
                        additionComparer.SwitchSides();
                    }
                }

                // Check for difference based comparison for the attribute-childs:
                CollectionComparer<XName, string> diffComparer = null;
                var needDiffComparison = PreParseInformation.CreateDifferenceComparer(
                    parentXmlPath,
                    XmlDifferences,
                    out diffComparer);

                // Populate the attribute-childs in the tree-view:
                foreach (var attribute in xNode.Attributes())
                {
                    var childnode = new TreeNode(
                        GetAttributeRepresentation(attribute),
                        imageIndex: 1,
                        selectedImageIndex: 1)
                        {
                            Tag = new NodeInformation(attribute)
                            {
                                XmlPath = ParsedXmlTree.CombinePath(
                                    parentXmlPath,
                                    MiscUtils.AdjustCaption(attribute.Name.ToString(), PreParseInformation.IgnoreNameCase, PreParseInformation.IgnoreWhiteSpaces)),
                                IsInBlockChange = nodeInformation.IsInBlockChange
                            }
                        };

                    if (needDiffComparison)
                    {
                        MarkNode(childnode, attribute.Name, diffComparer);
                    }

                    if (needAdditionComparison)
                    {
                        MarkNode(childnode, attribute.Name, additionComparer);
                    }

                    if (!needDiffComparison && !needAdditionComparison)
                    {
                        // This child attribute never got to be painted for color:
                        ColorNode(childnode);
                    }

                    node.Nodes.Add(childnode);
                }
            }

            // Create and mark child XML elements:
            foreach (var element in xNode.Elements())
            {
                string elementShortValue = MiscUtils.GetElementSingleValue(element);
                string elementValue = element.Name.LocalName +
                    (string.IsNullOrEmpty(elementShortValue)
                        ? string.Empty
                        : " => " + elementShortValue);
                string elementPath = ParsedXmlTree.CombinePath(parentXmlPath, element.Name.ToString());
                var childNode = new TreeNode(elementValue, imageIndex: 0, selectedImageIndex: 0)
                {
                    Tag = new NodeInformation(element)
                    {
                        XmlPath = KeyedXmlRecord.IsRecord(element)
                            ? new KeyedXmlRecord(PreParseInformation, elementPath, element).RecordPath
                            : elementPath,
                        IsInBlockChange = nodeInformation.IsInBlockChange
                    }
                };

                MarkNode(childNode, element);

                node.Nodes.Add(childNode);

                PopulateTreeElement(childNode, element);
            }

            // Mark this tree-node, if needed and accordingly:
            if (nodeInformation.ComparisonType == NodeRepresentation.Natural &&
                node.Nodes.Cast<TreeNode>().Any(
                    child => ((NodeInformation)child.Tag).ComparisonType != NodeRepresentation.Natural))
            {
                nodeInformation.ComparisonType = NodeRepresentation.GroupVariance;
                ColorNode(node);
            }
        }

        /// <summary>
        /// Marks a node corresponding to an XML attribute in a color matching its comparison value.
        /// </summary>
        /// <param name="node">The tree node to mark.</param>
        /// <param name="attributeName">The name of the attribute to mark.</param>
        /// <param name="comparer">A comparison information of two different XML records.</param>
        private void MarkNode(TreeNode node, XName attributeName, CollectionComparer<XName, string> comparer)
        {
            var nodeInformation = node.Tag as NodeInformation;

            // Adjust the attribute name:
            attributeName = MiscUtils.AdjustCaption(attributeName, PreParseInformation.IgnoreNameCase, PreParseInformation.IgnoreWhiteSpaces);

            if (nodeInformation == null)
            {
                Trace.TraceError("Cannot mark XML attribute tree-node {0}.", node.FullPath);

                return;
            }

            if ((comparer.OnlyOriginal.ContainsKey(attributeName) && FileViewerRole == FileComparisonRole.OriginSide) ||
                (comparer.OnlyTarget.ContainsKey(attributeName) && FileViewerRole == FileComparisonRole.TargetSide))
            {
                nodeInformation.ComparisonType = NodeRepresentation.OnlyHere;
                ColorNode(node);

                return;
            }

            if (comparer.Differences.ContainsKey(attributeName))
            {
                nodeInformation.ComparisonType = NodeRepresentation.Difference;
            }

            ColorNode(node);
        }

        /// <summary>
        /// Marks a node corresponding to an XML element in a color matching its comparison value.
        /// </summary>
        /// <param name="node">The tree node to mark.</param>
        /// <param name="xmlNode">A corresponding XML element for identification. If 'null', the identification does not include the XML contents.</param>
        private void MarkNode(TreeNode node, XElement xmlNode)
        {
            var nodeInformation = node.Tag as NodeInformation;

            if (nodeInformation == null)
            {
                Trace.TraceError("Cannot mark XML element tree-node {0}.", node.FullPath);

                return;
            }

            var xmlPath = MiscUtils.AdjustCaption(nodeInformation.XmlPath, PreParseInformation.IgnoreNameCase, PreParseInformation.IgnoreWhiteSpaces);
            KeyedXmlRecord pivot = xmlNode == null ? null : new KeyedXmlRecord(PreParseInformation, xmlPath, xmlNode);

            if (XmlOnlyHere != null &&
                XmlOnlyHere.ContainsKey(xmlPath) &&
                (xmlNode == null || XmlOnlyHere[xmlPath].Contains(pivot)))
            {
                if (XmlDifferences != null && XmlDifferences.ContainsKey(xmlPath))
                {
                    // The node is marked as a difference:
                    nodeInformation.ComparisonType = NodeRepresentation.Difference;
                }
                else
                {
                    // The node is marked as belonging to only one side of the comparison:
                    nodeInformation.ComparisonType = NodeRepresentation.OnlyHere;
                    nodeInformation.IsInBlockChange = true;
                }
            }

            if (XmlDifferences != null && XmlDifferences.ContainsKey(xmlPath))
            {
                // The node is just a difference:
                nodeInformation.ComparisonType = NodeRepresentation.Difference;
            }

            ColorNode(node);
        }

        /// <summary>
        /// Colorize the given tree-node.
        /// </summary>
        /// <param name="node">The tree-node to colorize, containing the information how to.</param>
        private void ColorNode(TreeNode node)
        {
            var nodeInformation = node.Tag as NodeInformation;

            if (nodeInformation == null)
            {
                Trace.TraceError("Could not colorize node '{0}', no information.", node.Text);

                return;
            }

            switch (nodeInformation.ComparisonType)
            {
                case NodeRepresentation.Natural:
                {
                    node.ForeColor = s_naturalForeColor;

                    break;
                }

                case NodeRepresentation.OnlyHere:
                {
                    switch (FileViewerRole)
                    {
                        case FileComparisonRole.OriginSide:
                        {
                            node.ForeColor = s_onlyOriginalColor;

                            break;
                        }

                        case FileComparisonRole.TargetSide:
                        {
                            node.ForeColor = s_onlyTargetColor;

                            break;
                        }

                        default:
                        {
                            LogUi.LogFatal("Unrecognized 'FileComparisonRole' value '{0}'", FileViewerRole);

                            break;
                        }
                    }

                    break;
                }

                case NodeRepresentation.Difference:
                {
                    node.ForeColor = s_differentColor;

                    break;
                }

                case NodeRepresentation.GroupVariance:
                {
                    node.BackColor = s_groupVarianceColor;

                    break;
                }

                default:
                {
                    LogUi.LogFatal("Unrecognized 'NodeRepresentation' value '{0}'", nodeInformation);

                    break;
                }
            }

            node.BackColor = nodeInformation.IsSearchResult
                ? s_searchMark
                : nodeInformation.IsCounterChosen
                    ? s_counterPartDiffColor
                    : nodeInformation.IsLastSelected
                        ? s_selectedLostFocus
                        : nodeInformation.IsInBlockChange
                            ? FileViewerRole == FileComparisonRole.OriginSide
                                ? s_blockRemoved
                                : s_blockAdded
                            : node.BackColor != s_groupVarianceColor
                                ? s_naturalBackColor
                                : node.BackColor;
        }

        /// <summary>
        /// Find and enumerate tree-nodes that match a given criteria - down the tree.
        /// </summary>
        /// <param name="direction">The direction to search.</param>
        /// <param name="startNode">The tree-node where to start the search with. When the recursion hits this node, the enumeration starts.</param>
        /// <param name="criteria">The predicate for the criteria, given the scanned tree-node.</param>
        /// <returns>The tree-nodes enumeration.</returns>
        private IEnumerable<TreeNode> FindNodeByCriteria(SearchDirection direction, TreeNode startNode, Predicate<TreeNode> criteria)
        {
            // Assuming there's exactly one root node.
            var treeRoot = treeXml.Nodes[0];

            m_searchHitStartPoint = startNode == treeRoot;

            return direction == SearchDirection.Forward
                ? FindNextNodeByCriteria(treeRoot, startNode, criteria)
                : FindPrevNodeByCriteria(treeRoot, startNode, criteria);
        }

        /// <summary>
        /// Find and enumerate tree-nodes that match a given criteria - down the tree.
        /// </summary>
        /// <param name="rootNode">The tree-node marking the current traversed node in the search recursion.</param>
        /// <param name="startNode">The tree-node where to start the search with. When the recursion hits this node, the enumeration starts.</param>
        /// <param name="criteria">The predicate for the criteria, given the scanned tree-node.</param>
        /// <returns>The tree-nodes enumeration.</returns>
        private IEnumerable<TreeNode> FindNextNodeByCriteria(TreeNode rootNode, TreeNode startNode, Predicate<TreeNode> criteria)
        {
            if (rootNode == startNode)
            {
                m_searchHitStartPoint = true;
            }
            else
            {
                if (m_searchHitStartPoint && criteria(rootNode))
                {
                    yield return rootNode;
                }
            }

            // Traverse the child nodes:
            foreach (var child in rootNode.Nodes.Cast<TreeNode>().SelectMany(node => FindNextNodeByCriteria(node, startNode, criteria)))
            {
                yield return child;
            }
        }

        /// <summary>
        /// Find and enumerate tree-nodes that match a given criteria - up the tree.
        /// </summary>
        /// <param name="rootNode">The tree-node marking the current traversed node in the search recursion.</param>
        /// <param name="startNode">The tree-node where to start the search with. When the recursion hits this node, the enumeration starts.</param>
        /// <param name="criteria">The predicate for the criteria, given the scanned tree-node.</param>
        /// <returns>The tree-nodes enumeration.</returns>
        private IEnumerable<TreeNode> FindPrevNodeByCriteria(TreeNode rootNode, TreeNode startNode, Predicate<TreeNode> criteria)
        {
            foreach (var child in rootNode.Nodes.Cast<TreeNode>().Reverse().SelectMany(node => FindPrevNodeByCriteria(node, startNode, criteria)))
            {
                yield return child;
            }

            if (rootNode == startNode)
            {
                m_searchHitStartPoint = true;
            }
            else
            {
                if (m_searchHitStartPoint && criteria(rootNode))
                {
                    yield return rootNode;
                }
            }
        }

        /// <summary>
        /// Calculates the way to display a given XML attribute.
        /// </summary>
        /// <param name="attribute">The attribute to represent.</param>
        /// <returns>The string representation.</returns>
        private string GetAttributeRepresentation(XAttribute attribute)
        {
            return ViewXmlSyntax
                ? string.Format(CultureInfo.InvariantCulture, "{0}=\"{1}\"", attribute.Name.LocalName, attribute.Value)
                : string.Join(" => ", attribute.Name.LocalName, attribute.Value);
        }

        /// <summary>
        /// Retrieves a tree node to use, either existing, or new allocated one.
        /// </summary>
        /// <param name="nodes">The enumeration of tree-nodes where to take the existing tree-node from.</param>
        /// <param name="nodeName">The name of the required new node.</param>
        /// <param name="nodeValue">The value of the required node.</param>
        /// <param name="imageIndex">The image index to use with the new node.</param>
        /// <param name="selectedImageIndex">The index of the image to use when the new node is selected.</param>
        /// <returns>The required tree-node to use.</returns>
        private TreeNode DetermineTreeNode(
            IEnumerable<TreeNode> nodes,
            string nodeName,
            string nodeValue,
            int imageIndex,
            int selectedImageIndex)
        {
            return null;
        }

        #endregion

        private void buttonChooseFile_Click(object sender, EventArgs e)
        {
            UserChooseFileInput();
        }

        private void labeledTextFilePath_Validated(object sender, EventArgs e)
        {
                if (labeledTextFilePath.InternalText.Equals(FilePath.Name))
            {
                return;
            }

            // Since the file path changed, the label is no longer valid:
            FilePath = new NameAndLabel(labeledTextFilePath.InternalText, null);
        }

        private void expandAllToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var selectedNode = treeXml.SelectedNode;

            if (selectedNode != null)
            {
                selectedNode.ExpandAll();
                selectedNode.EnsureVisible();
            }
        }

        private void collapseAllToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var selectedNode = treeXml.SelectedNode;

            if (selectedNode != null)
            {
                selectedNode.Collapse();
                selectedNode.EnsureVisible();
            }
        }

        private void expandHereToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var selectedNode = treeXml.SelectedNode;

            if (selectedNode != null)
            {
                selectedNode.Expand();
                selectedNode.EnsureVisible();
            }
        }

        private void showPathToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var selectedNode = treeXml.SelectedNode;

            if (selectedNode != null)
            {
                LogUi.LogInfo(
                    "Tree representation path: {0}{1}XML path: {2}",
                    selectedNode.FullPath,
                    Environment.NewLine,
                    ((NodeInformation)selectedNode.Tag).XmlPath);
            }
        }

        private void treeXml_AfterSelect(object sender, TreeViewEventArgs e)
        {
            var selectedNode = treeXml.SelectedNode;

            if (selectedNode != null)
            {
                var selectedNodeInformation = selectedNode.Tag as NodeInformation;

                if (selectedNodeInformation != null)
                {
                    ClearNodeCollection(m_searchResult, node => node.IsSearchResult = false);

                    selectedNodeInformation.IsLastSelected = true;
                    m_lastSelectedNode = selectedNode;
                    ColorNode(selectedNode);

                    // Notify of a diff-node that was selected:
                    OnDiffNodeSelected(this, selectedNodeInformation, Controller.IsControlPressed);
                }
            }

            ClearNodeCollection(m_markedNodes, node => node.IsCounterChosen = false);
        }

        private void treeXml_Leave(object sender, EventArgs e)
        {
            treeXml.SelectedNode = null;
        }

        private void buttonNextDiff_Click(object sender, EventArgs e)
        {
            if (treeXml.Nodes.Count > 0)
            {
                ClearNodeCollection(m_searchResult, node => node.IsSearchResult = false);

                var currNode = treeXml.SelectedNode ?? treeXml.Nodes[0];
                var iterator = FindNodeByCriteria(
                    SearchDirection.Forward,
                    currNode,
                    node => ((NodeInformation)node.Tag).ComparisonType != NodeRepresentation.Natural &&
                            ((NodeInformation)node.Tag).ComparisonType != NodeRepresentation.GroupVariance)
                        .GetEnumerator();

                if (iterator.MoveNext())
                {
                    treeXml.SelectedNode = iterator.Current;
                    ((NodeInformation)iterator.Current.Tag).IsSearchResult = true;

                    m_searchResult.Add(iterator.Current);
                    ColorNode(iterator.Current);
                }
                else
                {
                    LogUi.LogInfo("Reached the end of the XML tree, there are no more differences.");
                }
            }
        }

        private void buttonPrevDiff_Click(object sender, EventArgs e)
        {
            if (treeXml.Nodes.Count > 0)
            {
                ClearNodeCollection(m_searchResult, node => node.IsSearchResult = false);

                var currNode = treeXml.SelectedNode ?? treeXml.Nodes[0];
                var iterator =
                    FindNodeByCriteria(
                        SearchDirection.Backward,
                        currNode,
                        node => ((NodeInformation)node.Tag).ComparisonType != NodeRepresentation.Natural &&
                                ((NodeInformation)node.Tag).ComparisonType != NodeRepresentation.GroupVariance)
                            .GetEnumerator();

                if (iterator.MoveNext())
                {
                    treeXml.SelectedNode = iterator.Current;
                    ((NodeInformation)iterator.Current.Tag).IsSearchResult = true;

                    m_searchResult.Add(iterator.Current);
                    ColorNode(iterator.Current);
                }
                else
                {
                    LogUi.LogInfo("Reached the start of the XML tree, there are no more differences.");
                }
            }
        }

        /// <summary>
        /// TODO: Activate search
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void findToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var selectedNode = treeXml.SelectedNode ?? treeXml.Nodes[0];
            var searchDialog = new FindForm();

            if (searchDialog.ShowDialog() == DialogResult.OK && !string.IsNullOrEmpty(searchDialog.textSearch.Text))
            {
                ClearNodeCollection(m_searchResult, node => node.IsSearchResult = false);

                var iterator = FindNodeByCriteria(
                    SearchDirection.Forward,
                    selectedNode,
                    node => node.Text.Contains(searchDialog.textSearch.Text))
                        .GetEnumerator();

                if (iterator.MoveNext())
                {
                    treeXml.SelectedNode = iterator.Current;
                    ((NodeInformation)iterator.Current.Tag).IsSearchResult = true;

                    m_searchResult.Add(iterator.Current);
                    ColorNode(iterator.Current);
                }
                else
                {
                    LogUi.LogInfo("Reached the start of the XML tree, there are no more results.");
                }
            }
        }

        private void xMLQualifiedNameToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var nodeInfo = treeXml.SelectedNode.Tag as NodeInformation;

            if (nodeInfo != null)
            {
                MiscUtils.ExecuteOnXmlObject(
                    nodeInfo.XmlObject,
                    element => Clipboard.SetText(element.Name.ToString()),
                    attribute => Clipboard.SetText(attribute.Name.ToString()));
            }
        }

        private void xMLLocalNameToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var nodeInfo = treeXml.SelectedNode.Tag as NodeInformation;

            if (nodeInfo != null)
            {
                MiscUtils.ExecuteOnXmlObject(
                    nodeInfo.XmlObject,
                    element => Clipboard.SetText(element.Name.LocalName),
                    attribute => Clipboard.SetText(attribute.Name.LocalName));
            }
        }

        private void xMLValueToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var nodeInfo = treeXml.SelectedNode.Tag as NodeInformation;

            if (nodeInfo != null)
            {
                MiscUtils.ExecuteOnXmlObject(
                    nodeInfo.XmlObject,
                    element => Clipboard.SetText(MiscUtils.GetElementSingleValue(element)),
                    attribute => Clipboard.SetText(attribute.Value));
            }
        }

        private void treeXml_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
        {
            if (e.Button == MouseButtons.Right)
            {
                treeXml.SelectedNode = e.Node;
            }
        }

        private void treeXml_BeforeSelect(object sender, TreeViewCancelEventArgs e)
        {
            if (m_lastSelectedNode != null)
            {
                var lastSelectedNodeInfo = m_lastSelectedNode.Tag as NodeInformation;

                if (lastSelectedNodeInfo != null)
                {
                    // Remove the selection indication from this tree-node which is about to lose selection indication:
                    lastSelectedNodeInfo.IsLastSelected = false;

                    ColorNode(m_lastSelectedNode);
                }
            }
        }

        private void xMLTextToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var nodeInfo = treeXml.SelectedNode.Tag as NodeInformation;

            if (nodeInfo != null)
            {
                Clipboard.SetText(nodeInfo.XmlObject.ToString());
            }
        }
    }

    /// <summary>
    /// Additional information to attach to a tree-node, by its 'Tag' property.
    /// </summary>
    public class NodeInformation
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="NodeInformation"/> class, setting it to default values.
        /// </summary>
        /// <param name="xmlObject">The XML object relevant to this tree node.</param>
        public NodeInformation(XObject xmlObject)
        {
            XmlObject = xmlObject;

            ComparisonType = NodeRepresentation.Natural;
            IsCounterChosen = false;
            IsSearchResult = false;
            IsInBlockChange = false;
            IsLastSelected = false;
        }

        /// <summary>
        /// Gets the relevant XML object for this tree node.
        /// </summary>
        public XObject XmlObject { get; private set; }

        /// <summary>
        /// Gets or sets the type of comparison information this tree-node represents.
        /// </summary>
        public NodeRepresentation ComparisonType { get; set; }

        /// <summary>
        /// Gets or sets the XML path corresponding to this tree-node.
        /// </summary>
        public string XmlPath { get; set; }

        /// <summary>
        /// Gets whether this node shows it is a counter-part of the difference selected by the user on the other side of the comparison.
        /// </summary>
        public bool IsCounterChosen { get; set; }

        /// <summary>
        /// Gets whether this node is a search result.
        /// </summary>
        public bool IsSearchResult { get; set; }

        /// <summary>
        /// Gets or sets whether this node has some ancestor which was added or removed.
        /// </summary>
        public bool IsInBlockChange { get; set; }

        /// <summary>
        /// Gets or sets whether this node was selected before this <see cref="TreeView"/> control lost focus.
        /// </summary>
        public bool IsLastSelected { get; set; }
    }
}
